Skip to main content

Overview

DevolutionSync implements a clean MVC (Model-View-Controller) architecture that separates business logic, data access, and presentation layers. This pattern ensures maintainability, testability, and scalability.

MVC Pattern Diagram

┌──────────────────────────────────────────────────┐
│                   USER REQUEST                    │
│            (HTTP via Browser)                     │
└────────────────────┬─────────────────────────────┘


┌────────────────────────────────────────────────────┐
│              FRONT CONTROLLER                      │
│                 (index.php)                        │
│  • Parse URL                                       │
│  • Load Controller                                 │
│  • Execute Method                                  │
└────────────────────┬───────────────────────────────┘


┌────────────────────────────────────────────────────┐
│                  CONTROLLER                        │
│        (AuthController, AdminController)           │
│  • Receive Request                                 │
│  • Validate Input                                  │
│  • Call Model Methods                              │
│  • Prepare Data                                    │
│  • Load View                                       │
└──────────┬────────────────────┬────────────────────┘
           │                    │
           ▼                    ▼
┌──────────────────┐  ┌──────────────────────────┐
│      MODEL       │  │         VIEW             │
│ (DevolucionModel)│  │  (panel_administrador)   │
│  • Database      │  │  • HTML Template         │
│  • Business Logic│  │  • Display Data          │
│  • Data Access   │  │  • User Interface        │
└──────────┬───────┘  └──────────────────────────┘


┌──────────────────────────────────────────────────┐
│               DATABASE (MySQL)                    │
│         devoluciones, usuarios, etc.              │
└───────────────────────────────────────────────────┘

Directory Structure

DevolutionSync/
├── index.php                    # Front Controller
├── Controllers/
   ├── AuthController.php       # Authentication
   ├── AdminController.php      # Administrator panel
   ├── PanelController.php      # User dashboard
   ├── ConsultaController.php   # Query/search
   ├── HomeController.php       # Home page
   └── UsuarioController.php    # User management
├── Models/
   ├── AuthModel.php            # Authentication logic
   ├── DevolucionModel.php      # Deviation management
   ├── ProductoModel.php        # Product catalog
   ├── ConsultaModel.php        # Query operations
   └── UsuarioModel.php         # User operations
├── Views/
   ├── auth/
   └── login.php            # Login page
   ├── admin/
   ├── panel_administrador.php
   └── estadisticas.php
   ├── panel/
   └── dashboard.php
   └── consulta/
       └── historial.php
└── Config/
    └── Conexion.php             # Database connection

Controllers Layer

Purpose

Controllers handle HTTP requests, coordinate between models and views, and contain application flow logic.

Controller Base Structure

<?php
require_once 'Models/AuthModel.php';

class AuthController {
    private $model;

    public function __construct() {
        if (session_status() === PHP_SESSION_NONE) session_start();
        $this->model = new AuthModel();
    }

    public function index() {
        if (isset($_SESSION['logged_in'])) {
            $this->redirigirSegunGrado($_SESSION['grado']);
            return;
        }
        require_once 'Views/auth/login.php';
    }

    public function login() {
        header('Content-Type: application/json');
        
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $username = trim($_POST['username'] ?? '');
            $password = $_POST['password'] ?? '';

            $user = $this->model->buscarUsuario($username);

            if ($user && $password === $user['PAS']) {
                $_SESSION['user'] = $user['USR'];
                $_SESSION['nombre'] = $user['NOMBRE'];
                $_SESSION['grado'] = $user['GRADO'];
                $_SESSION['logged_in'] = true;
                $_SESSION['last_activity'] = time();
                
                session_regenerate_id(true);

                echo json_encode([
                    'success' => true,
                    'redirect' => $this->getRedirectUrl($user['GRADO'])
                ]);
            } else {
                echo json_encode([
                    'success' => false, 
                    'message' => 'Credenciales incorrectas'
                ]);
            }
        }
    }

    public function logout() {
        session_unset();
        session_destroy();
        header('Location: index.php?url=auth/index');
        exit;
    }

    private function getRedirectUrl($grado) {
        switch ($grado) {
            case 1: 
                return 'index.php?url=home/index'; 
            case 2: 
                return 'index.php?url=devolucion/crear'; 
            case 3: 
                return 'index.php?url=consulta/index';
            default: 
                return 'index.php?url=auth/index';
        }
    }

    private function redirigirSegunGrado($grado) {
        header('Location: ' . $this->getRedirectUrl($grado));
        exit;
    }
}

Controller Responsibilities

Request Handling

  • Receive HTTP requests
  • Parse GET/POST parameters
  • Validate input data
  • Handle file uploads

Business Logic

  • Call model methods
  • Process data
  • Apply business rules
  • Handle transactions

Security

  • Session management
  • Authentication checks
  • Authorization validation
  • CSRF protection

Response

  • Load views
  • Return JSON
  • Handle redirects
  • Set headers

Controller Naming Convention

Pattern: {Entity}Controller.php
  • Class name: {Entity}Controller
  • File name: {Entity}Controller.php
  • Location: Controllers/
  • Example: AdminController.php contains class AdminController

Models Layer

Purpose

Models handle data access, database operations, and contain business logic related to data manipulation.

Model Base Structure

<?php
require_once 'Config/Conexion.php';

class DevolucionModel {
    private $db;

    public function __construct() {
        $this->db = Conexion::Conectar();
    }

    // ========================================
    // CREATE - Insert new deviation
    // ========================================
    
    public function guardar($datos) {
        $sql = "INSERT INTO devoluciones (
                    nit, nombre_cliente, direccion, item_producto, 
                    descripcion_producto, unidad, kg, motivo, 
                    cantidad_und, cantidad_kg, observacion, 
                    usuario_creador, estado, fecha_creacion, evidencia
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Pendiente', NOW(), ?)";
        
        $stmt = $this->db->prepare($sql);
        
        return $stmt->execute([
            $datos['nit'],
            $datos['nombre_cliente'],
            $datos['direccion'],
            $datos['item_producto'],
            $datos['descripcion_producto'],
            $datos['unidad'],
            $datos['kg'],
            $datos['motivo'],
            $datos['cantidad_und'],
            $datos['cantidad_kg'],
            $datos['observacion'],
            $datos['usuario_creador'],
            $datos['evidencia'] ?? null
        ]);
    }

    // ========================================
    // READ - Get pending deviations
    // ========================================

    public function obtenerPendientes() {
        $stmt = $this->db->prepare(
            "SELECT * FROM devoluciones 
             WHERE estado = 'Pendiente' 
             ORDER BY fecha_creacion ASC"
        );
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    // ========================================
    // UPDATE - Process admin review
    // ========================================

    public function procesarRevision($id, $accion, $codigo, $obs, $revisor) {
        try {
            $this->db->beginTransaction();

            $sql = "UPDATE devoluciones 
                    SET estado = ?, 
                        codigo_admin = ?, 
                        observacion_admin = ?, 
                        usuario_revisor = ?, 
                        fecha_revision = NOW() 
                    WHERE id = ?";
            
            $stmt = $this->db->prepare($sql);
            $stmt->execute([$accion, $codigo, $obs, $revisor, $id]);

            $this->db->commit();
            return true;
        } catch (Exception $e) {
            $this->db->rollBack();
            return false;
        }
    }

    // ========================================
    // STATISTICS - Dashboard data
    // ========================================
    
    public function obtenerEstadisticas($fecha = null) {
        $where = $fecha ? "WHERE DATE(fecha_creacion) = :fecha" : "";
        
        $sql = "SELECT 
                    COUNT(*) as total,
                    COALESCE(SUM(cantidad_kg), 0) as total_kg,
                    COALESCE(SUM(cantidad_und), 0) as total_und,
                    
                    COUNT(CASE WHEN estado = 'Pendiente' THEN 1 END) as pendientes,
                    COUNT(CASE WHEN estado = 'Aprobado' THEN 1 END) as aprobados,
                    COUNT(CASE WHEN estado = 'Rechazado' THEN 1 END) as rechazados,
                    
                    COUNT(CASE WHEN motivo = 'Devolucion' THEN 1 END) as motivo_dev,
                    COUNT(CASE WHEN motivo = 'Faltante' THEN 1 END) as motivo_fal,
                    COUNT(CASE WHEN motivo = 'Sobrante' THEN 1 END) as motivo_sob
                FROM devoluciones 
                $where";

        $stmt = $this->db->prepare($sql);
        if ($fecha) $stmt->bindValue(':fecha', $fecha);
        $stmt->execute();
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function obtenerFechas() {
        $stmt = $this->db->query(
            "SELECT DISTINCT DATE(fecha_creacion) as fecha 
             FROM devoluciones 
             ORDER BY fecha DESC"
        );
        return $stmt->fetchAll(PDO::FETCH_COLUMN);
    }
}

Model Responsibilities

  • Execute SQL queries
  • Use prepared statements
  • Handle database connections
  • Return result sets
  • Create: Insert new records
  • Read: Query and fetch data
  • Update: Modify existing records
  • Delete: Remove records (soft/hard)
  • Data validation
  • Calculations and aggregations
  • Transaction management
  • Data transformations
  • Hide SQL complexity
  • Provide clean interfaces
  • Return formatted data
  • Handle database errors

PDO Connection Pattern

All models use the Conexion class for database access:
private $db;

public function __construct() {
    $this->db = Conexion::Conectar();
}
The Conexion::Conectar() method returns a configured PDO instance with:
  • Exception error mode
  • Associative array fetch mode
  • Real prepared statements (not emulated)

Prepared Statements

$sql = "SELECT * FROM devoluciones WHERE id = ? AND estado = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([123, 'pendiente']);

Transaction Management

DevolucionModel.php (excerpt)
public function procesarRevision($id, $accion, $codigo, $obs, $revisor) {
    try {
        $this->db->beginTransaction();

        // Multiple database operations
        $stmt = $this->db->prepare($sql);
        $stmt->execute([...]);
        
        // More operations...

        $this->db->commit();
        return true;
    } catch (Exception $e) {
        $this->db->rollBack();
        return false;
    }
}
Transaction Best Practices:
  • Always use try-catch with transactions
  • Roll back on any error
  • Keep transactions short
  • Avoid user input during transactions

Views Layer

Purpose

Views handle presentation logic and render HTML templates with data provided by controllers.

View Structure Example

Views/admin/panel_administrador.php (excerpt)
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title><?= htmlspecialchars($titulo) ?></title>
    <link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
    <header>
        <h1>Panel de Administración</h1>
        <p>Usuario: <?= htmlspecialchars($_SESSION['nombre']) ?></p>
    </header>
    
    <main>
        <section class="pendientes">
            <h2>Devoluciones Pendientes (<?= count($pendientes) ?>)</h2>
            
            <?php if (empty($pendientes)): ?>
                <p class="no-data">No hay devoluciones pendientes</p>
            <?php else: ?>
                <table>
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Cliente</th>
                            <th>Producto</th>
                            <th>Motivo</th>
                            <th>Fecha</th>
                            <th>Acciones</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($pendientes as $dev): ?>
                            <tr>
                                <td><?= htmlspecialchars($dev['id']) ?></td>
                                <td><?= htmlspecialchars($dev['nombre_cliente']) ?></td>
                                <td><?= htmlspecialchars($dev['descripcion_producto']) ?></td>
                                <td><?= htmlspecialchars($dev['motivo']) ?></td>
                                <td><?= htmlspecialchars($dev['fecha_creacion']) ?></td>
                                <td>
                                    <button onclick="revisar(<?= $dev['id'] ?>)">
                                        Revisar
                                    </button>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>
        </section>
    </main>
    
    <script src="assets/js/admin.js"></script>
</body>
</html>

View Responsibilities

Presentation

  • Render HTML
  • Display data
  • Format output
  • Apply styling

User Interface

  • Forms
  • Tables
  • Buttons
  • Navigation

Data Display

  • Loop through arrays
  • Conditional rendering
  • Format dates/numbers
  • Escape output

Client-Side

  • Include JavaScript
  • Include CSS
  • AJAX calls
  • Event handlers

XSS Prevention

Always escape user data in views:
<!-- GOOD: Escaped output -->
<p>Cliente: <?= htmlspecialchars($cliente['nombre']) ?></p>

<!-- BAD: Direct output (XSS vulnerability) -->
<p>Cliente: <?= $cliente['nombre'] ?></p>

<!-- GOOD: Escaped attribute -->
<input type="text" value="<?= htmlspecialchars($producto['nombre']) ?>">

<!-- GOOD: Loop with escaping -->
<?php foreach ($items as $item): ?>
    <li><?= htmlspecialchars($item['descripcion']) ?></li>
<?php endforeach; ?>

View Loading Pattern

Controllers load views using require_once:
public function index() {
    $titulo = "Panel Administrador";
    $pendientes = $this->model->obtenerPendientes();
    $historial = $this->consultaModel->obtenerHistorial(50);
    
    // Variables are available in the view
    require_once 'Views/admin/panel_administrador.php';
}
Variables defined in the controller method are automatically available in the required view file.

Data Flow Example

Complete Request-Response Cycle

1

User Submits Form

User clicks “Aprobar” button on admin panel
fetch('index.php?url=admin/revisar', {
    method: 'POST',
    body: formData
})
2

Front Controller Routes

index.php parses URL and loads AdminController
$controllerName = 'AdminController';
$method = 'revisar';
$controller->revisar();
3

Controller Validates

AdminController::revisar() validates input
$id = intval($_POST['id_devolucion'] ?? 0);
$accion = trim($_POST['accion'] ?? '');
if (!in_array($accion, ['aprobado', 'rechazado'])) {
    throw new Exception('Invalid action');
}
4

Model Updates Database

DevolucionModel::procesarRevision() executes SQL
$this->db->beginTransaction();
$stmt->execute([$accion, $codigo, $obs, $revisor, $id]);
$this->db->commit();
5

Controller Responds

Redirect to admin panel with success message
header('Location: index.php?url=admin/index&msg=success');
6

View Displays Result

Admin panel shows updated data
$pendientes = $this->model->obtenerPendientes();
require_once 'Views/admin/panel_administrador.php';

Best Practices

Controller Best Practices

Controllers should orchestrate, not implement business logic:
// ❌ BAD: Too much logic in controller
public function aprobar() {
    $sql = "UPDATE devoluciones SET estado = 'aprobado' WHERE id = ?";
    $stmt = $db->prepare($sql);
    $stmt->execute([$id]);
}

// ✅ GOOD: Delegate to model
public function aprobar() {
    $resultado = $this->model->aprobarDevolucion($id);
    if ($resultado) {
        header('Location: index.php?url=admin/index&msg=success');
    }
}
// ✅ GOOD: Thorough validation
$id = intval($_POST['id_devolucion'] ?? 0);
if ($id <= 0) {
    throw new Exception('Invalid ID');
}

$accion = trim($_POST['accion'] ?? '');
if (!in_array($accion, ['aprobado', 'rechazado'])) {
    throw new Exception('Invalid action');
}
public function __construct() {
    if (session_status() === PHP_SESSION_NONE) session_start();
    
    // Check authentication first
    if (!isset($_SESSION['logged_in']) || $_SESSION['grado'] != 1) {
        header('Location: index.php?url=auth/index');
        exit;
    }
}

Model Best Practices

// ✅ GOOD: Prepared statement
$stmt = $this->db->prepare("SELECT * FROM devoluciones WHERE id = ?");
$stmt->execute([$id]);

// ❌ BAD: String interpolation (SQL injection!)
$sql = "SELECT * FROM devoluciones WHERE id = $id";
$this->db->query($sql);
public function complexOperation() {
    try {
        $this->db->beginTransaction();
        
        // Multiple operations
        $this->updateDevolucion($id);
        $this->createNotificacion($userId);
        
        $this->db->commit();
        return true;
    } catch (Exception $e) {
        $this->db->rollBack();
        error_log($e->getMessage());
        return false;
    }
}
// ✅ GOOD: Consistent return type
public function obtenerPorId($id) {
    $stmt = $this->db->prepare("SELECT * FROM devoluciones WHERE id = ?");
    $stmt->execute([$id]);
    $result = $stmt->fetch();
    return $result ?: null; // Always return array or null
}

View Best Practices

<!-- GOOD: Escaped -->
<p><?= htmlspecialchars($data['nombre']) ?></p>

<!-- BAD: Not escaped (XSS vulnerability) -->
<p><?= $data['nombre'] ?></p>
<!-- BAD: Database query in view -->
<?php
$stmt = $db->query("SELECT * FROM devoluciones");
$data = $stmt->fetchAll();
?>

<!-- GOOD: Data passed from controller -->
<?php foreach ($pendientes as $item): ?>
    <li><?= htmlspecialchars($item['nombre']) ?></li>
<?php endforeach; ?>
<!-- GOOD: Reusable header -->
<?php require_once 'Views/partials/header.php'; ?>

<main>
    <!-- Page content -->
</main>

<?php require_once 'Views/partials/footer.php'; ?>

Testing Considerations

Unit Testing Models

class DevolucionModelTest extends PHPUnit\Framework\TestCase {
    private $model;
    
    public function setUp(): void {
        $this->model = new DevolucionModel();
    }
    
    public function testObtenerPendientes() {
        $result = $this->model->obtenerPendientes();
        $this->assertIsArray($result);
    }
    
    public function testGuardarDevolucion() {
        $datos = [
            'nit' => '900123456',
            'nombre_cliente' => 'Test Cliente',
            // ... more fields
        ];
        $result = $this->model->guardar($datos);
        $this->assertTrue($result);
    }
}

Next Steps

Architecture Overview

Understand the complete system architecture

Database Schema

Explore the database structure

API Reference

View all controller and model methods

Deployment

Deploy with Docker